Original Note

3.3 Code Decoupling & Abstractions - Read

3.3 Code Decoupling & Abstractions(整理版)

原始笔记: Code Decoupling.md 原始教程: 3.3 Code Decoupling & Abstractions created: 2026-07-27 15:19 整理说明: 原笔记指定“整理原始教程的练习和解答”。本版本保留该边界,整理教程中的五组练习与参考解答,并只加入完成练习所需的概念说明。

内容简要概括

代码解耦通过拆分职责并减少组件间依赖,使代码更容易理解、测试和维护。五组练习从提取数据加载函数开始,逐步引入类、接口和多态,再用 JSON 数据源与 mock 证明:只要不同实现遵守同一接口,分析逻辑就不必随数据来源变化。

代码解耦、AbstractionEncapsulationInterfacePolymorphismCSVDataSourceJSONDataSourceMock、依赖注入、可测试性

目录


1. 练习所需概念

1.1 Code Decoupling

代码解耦是把软件拆分成更小的组件,并降低组件之间的相互依赖。如果一个组件发生变化时,另一个组件不必同步修改,就说明两者的耦合较低。

松耦合通常带来三个直接好处:

  • 更容易理解:一次只需要关注一个较小组件;
  • 更容易测试:可以独立测试组件,或用替代实现隔离外部依赖;
  • 更容易维护:修改更容易限制在单一职责范围内。

1.2 Abstraction 与 Encapsulation

Abstraction 把实现细节隐藏在接口之后,让调用者关注“它能做什么”,而不是“它内部如何完成”。

Encapsulation 把数据与操作这些数据的方法组织在同一组件中,并限制外部直接依赖内部表示。

可以简记为:

  • Encapsulation:隐藏内部信息;
  • Abstraction:隐藏实现方式。

1.3 Interface 与 Polymorphism

接口描述一个组件可以执行哪些操作、需要哪些参数、返回什么结果。调用者只依赖接口,不必依赖某个具体实现。

Polymorphism 允许不同类型通过同一接口被使用。例如,CSV 数据源和 JSON 数据源可以使用不同的内部实现,但都提供 load_inflammation_data();分析函数因此可以用同一种方式处理两者。

2. 练习一:分离数据加载与分析

2.1 练习

修改 compute_data.py

  1. analyse_data() 中提取数据加载逻辑;
  2. 新建 load_inflammation_data(dir_path)
  3. 让该函数读取指定目录中的炎症 CSV 文件;
  4. 返回由二维 NumPy 数组组成的列表;
  5. analyse_data() 调用新的加载函数。

目标是把“从文件读取数据”和“对数据执行分析”分成两个职责。

2.2 参考解答

def load_inflammation_data(dir_path):
    data_file_paths = glob.glob(
        os.path.join(dir_path, "inflammation*.csv")
    )
    if len(data_file_paths) == 0:
        raise ValueError(
            f"No inflammation CSV files found in path {dir_path}"
        )

    data = map(models.load_csv, data_file_paths)
    return list(data)

analyse_data() 只负责协调加载、计算与展示:

def analyse_data(data_dir):
    data = load_inflammation_data(data_dir)

    means_by_day = map(models.daily_mean, data)
    means_by_day_matrix = np.stack(list(means_by_day))
    daily_standard_deviation = np.std(means_by_day_matrix, axis=0)

    graph_data = {
        "standard deviation by day": daily_standard_deviation,
    }
    views.visualize(graph_data)

这一修改完成了第一步职责分离,但 analyse_data() 仍然通过固定函数依赖 CSV 加载方式。

3. 练习二:用类抽象数据加载

3.1 练习

compute_data.py 中声明 CSVDataSource

  1. 构造函数接收数据目录路径;
  2. load_inflammation_data() 变为该类的方法;
  3. 在分析函数之外创建 CSVDataSource 实例;
  4. 把数据源对象传给 analyse_data()

修改后的分析函数应只向数据源请求数据:

def analyse_data(data_source):
    data = data_source.load_inflammation_data()
    ...

3.2 参考解答

class CSVDataSource:
    """Load inflammation data from CSV files in a directory."""

    def __init__(self, dir_path):
        self.dir_path = dir_path

    def load_inflammation_data(self):
        data_file_paths = glob.glob(
            os.path.join(self.dir_path, "inflammation*.csv")
        )
        if len(data_file_paths) == 0:
            raise ValueError(
                f"No inflammation CSV files found in path {self.dir_path}"
            )

        data = map(models.load_csv, data_file_paths)
        return list(data)

控制器负责创建具体数据源:

data_source = CSVDataSource(os.path.dirname(infiles[0]))
analyse_data(data_source)

分析函数接收数据源对象:

def analyse_data(data_source):
    data = data_source.load_inflammation_data()
    ...

现在,analyse_data() 不再知道数据来自哪个目录,也不再直接依赖 CSV 文件匹配逻辑。具体加载方式被封装在 CSVDataSource 中。

4. 练习三:识别数据源接口

4.1 练习

识别 CSVDataSourceanalyse_data() 之间的接口。需要回答:

  • analyse_data() 必须调用哪个方法;
  • 该方法接收哪些参数;
  • 该方法返回什么。

4.2 参考解答

两者之间的接口是:

load_inflammation_data()
    参数:无
    返回:list[2D NumPy array]

也就是说,传入 analyse_data() 的任意对象都应当:

  1. 提供 load_inflammation_data() 方法;
  2. 调用该方法时不需要额外参数;
  3. 返回一个列表,其中每个元素都是患者炎症数据的二维 NumPy 数组。

analyse_data() 只依赖这一行为约定,而不依赖对象一定是 CSVDataSource。这就是后续替换数据来源的基础。

5. 练习四:增加 JSON 数据源

5.1 练习

新增一个从 JSON 文件加载患者数据的类,并提供同样的 load_inflammation_data() 方法。

假设 models.py 中提供以下加载函数:

def load_json(filename):
    """Load NumPy arrays from a JSON document."""
    with open(filename, "r", encoding="utf-8") as file:
        data_as_json = json.load(file)
        return [
            np.array(entry["observations"])
            for entry in data_as_json
        ]

运行时应根据输入文件扩展名选择 CSV 或 JSON 数据源。

5.2 参考解答

class JSONDataSource:
    """Load inflammation data from JSON files in a directory."""

    def __init__(self, dir_path):
        self.dir_path = dir_path

    def load_inflammation_data(self):
        data_file_paths = glob.glob(
            os.path.join(self.dir_path, "inflammation*.json")
        )
        if len(data_file_paths) == 0:
            raise ValueError(
                f"No inflammation JSON files found in path {self.dir_path}"
            )

        data = map(models.load_json, data_file_paths)
        return list(data)

控制器根据扩展名选择实现:

_, extension = os.path.splitext(infiles[0])

if extension == ".json":
    data_source = JSONDataSource(os.path.dirname(infiles[0]))
elif extension == ".csv":
    data_source = CSVDataSource(os.path.dirname(infiles[0]))
else:
    raise ValueError(
        f"Unsupported data file format: {extension}"
    )

analyse_data(data_source)

CSVDataSourceJSONDataSource 的内部实现不同,但都遵守相同接口。因此,选择数据源的逻辑发生变化时,分析代码不需要修改。

6. 练习五:使用 Mock 测试

6.1 练习

tests/test_compute_data.py 中添加测试,用 mock 对象代替真实数据源:

  1. unittest.mock 导入 Mock
  2. 创建一个数据源 mock;
  3. 配置 load_inflammation_data() 的固定返回值;
  4. 把 mock 传给 analyse_data()
  5. 验证分析函数可以在不读取真实文件的情况下运行。

练习重点不是验证图形是否正确,而是验证 analyse_data() 能否通过约定接口使用替代数据源。

6.2 参考解答

from unittest.mock import Mock

def test_analyse_data_mock_source():
    from inflammation.compute_data import analyse_data

    data_source = Mock()
    mock_data = [
        [0, 2, 0](#broken-link-0%2C%202%2C%200),
        [0, 1, 0](#broken-link-0%2C%201%2C%200),
    ]
    data_source.load_inflammation_data.return_value = mock_data

    analyse_data(data_source)

测试不需要创建 CSV 或 JSON 文件,因为 mock 直接实现了测试所需的接口行为:

data_source.load_inflammation_data.return_value = mock_data

这种替代方式隔离了文件系统依赖,让测试只关注分析函数如何使用数据源。

7. 五组练习的递进关系

五组练习形成一条连续的设计改进路径:

从大函数提取数据加载职责
→ 用类封装具体加载方式
→ 明确分析函数依赖的最小接口
→ 用第二种实现验证多态
→ 用 Mock 验证可替换性和可测试性

最终结构可以概括为:

Controller
├── 根据输入选择 CSVDataSource 或 JSONDataSource
└── 把数据源传给 analyse_data()

analyse_data()
└── 只依赖 load_inflammation_data() 接口

Tests
└── 使用 Mock 提供同一接口的替代实现

核心不是“必须使用类”,而是让分析逻辑只依赖稳定、最小的接口,使数据加载、分析与测试能够独立变化。